const std = @import("std");
const print = std.debug.print;
const c = @cImport({
    @cInclude("net/if.h");
    @cInclude("linux/if_link.h");
});
const yazap = @import("yazap");
const config = @import("internal/config/config.zig");
const rule = @import("internal/rule/rule.zig");
const net = std.net;
const mem = std.mem;
const binary = std.binary;
pub const libbpf = rule.libbpf;

const debug = true;
var should_exit = std.atomic.Value(bool).init(false);

pub fn dbg_printf(level: libbpf.libbpf_print_level, fmt: [*c]const u8, args: @typeInfo(@typeInfo(@typeInfo(libbpf.libbpf_print_fn_t).optional.child).pointer.child).@"fn".params[2].type.?) callconv(.C) c_int {
    if (!debug and level == libbpf.LIBBPF_DEBUG) return 0;

    return libbpf.vdprintf(std.io.getStdErr().handle, fmt, args);
}

fn signal_handler(_: c_int) align(1) callconv(.C) void {
    std.debug.print("Received SIGINT\n", .{});

    should_exit.store(true, .seq_cst);
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();

    defer {
        const deinit_status = gpa.deinit();

        if (deinit_status == .leak) @panic("leaks detected");
    }

    var app = yazap.App.init(allocator, "ZephyrFirewall", "A eBPF firewall written in Zig");
    defer app.deinit();

    var root_cmd = app.rootCommand();

    try root_cmd.addArg(yazap.Arg.singleValueOption("ifname", 'i', "Network interface name"));

    const matches = try app.parseProcess();

    if (matches.getSingleValue("ifname") == null) {
        print("Please specify an network interface name.\n\n\n", .{});
        try app.displayHelp();
        return error.NO_IFNAME;
    }
    const ifname = matches.getSingleValue("ifname").?;

    const ifindex = c.if_nametoindex(@ptrCast(ifname));
    if (ifindex == 0) {
        print("failed to get index of network interface: {}\n", .{std.posix.errno(-1)});
        return error.DEV;
    }

    const conf = try config.get(allocator);
    defer conf.deinit(allocator);
    // print("FirewallConfig: {any}\n", .{conf});

    var sa = std.posix.Sigaction{
        .handler = .{
            .handler = &signal_handler,
        },
        .mask = std.posix.sigemptyset(),
        .flags = 0,
    };
    std.posix.sigaction(std.posix.SIG.INT, &sa, null);
    std.posix.sigaction(std.posix.SIG.TERM, &sa, null);

    const bytes = @embedFile("@bpf_prog");

    _ = libbpf.libbpf_set_print(dbg_printf);

    const obj = libbpf.bpf_object__open_mem(bytes.ptr, bytes.len, null);
    if (obj == null) {
        print("failed to open bpf object: {}\n", .{std.posix.errno(-1)});
        return error.OPEN;
    }
    defer libbpf.bpf_object__close(obj);

    var ret = libbpf.bpf_object__load(obj);
    if (ret != 0) {
        print("failed to load bpf object: {}\n", .{std.posix.errno(-1)});
        return error.LOAD;
    }

    const prog = libbpf.bpf_object__find_program_by_name(obj, "firewall").?;

    const prog_fd = libbpf.bpf_program__fd(prog);
    ret = libbpf.bpf_xdp_attach(@intCast(ifindex), prog_fd, c.XDP_FLAGS_UPDATE_IF_NOEXIST, null);
    if (ret < 0) {
        print("failed to attach program: {}\n", .{std.posix.errno(-1)});
        return error.ATTACH;
    }
    defer _ = libbpf.bpf_xdp_detach(@intCast(ifindex), c.XDP_FLAGS_UPDATE_IF_NOEXIST, null);

    try rule.processRules(obj.?, conf.rules);

    while (!should_exit.load(.seq_cst)) {
        std.debug.print("Working...\n", .{});
        std.time.sleep(1 * std.time.ns_per_s);
    }

    std.debug.print("Exiting gracefully...\n", .{});
}
